import functools
import itertools
import logging
import operator

from asgiref.sync import async_to_sync
from channels.db import database_sync_to_async
from channels.layers import get_channel_layer
from django.core.exceptions import ValidationError
from django.db import models, transaction
from django.db.models import Prefetch
from django.utils import timezone

from sysreptor.pentests.collab.consumer_base import CollabConsumerBase, GenericCollabMixin
from sysreptor.pentests.models import (
    CollabEvent,
    CollabEventType,
    Comment,
    CommentAnswer,
    NoteType,
    PentestFinding,
    PentestProject,
    ProjectNotebookPage,
    ReportSection,
    ShareInfo,
    UserNotebookPage,
    collab_context,
)
from sysreptor.pentests.permissions import ProjectSubresourcePermissions
from sysreptor.pentests.serializers.notes import ProjectNotebookPageSerializer, UserNotebookPageSerializer
from sysreptor.pentests.serializers.project import (
    CommentCreateSerializer,
    CommentSerializer,
    PentestFindingSerializer,
    ReportSectionSerializer,
)
from sysreptor.utils import license
from sysreptor.utils.configuration import configuration
from sysreptor.utils.fielddefinition.types import FieldDataType
from sysreptor.utils.fielddefinition.utils import (
    get_field_value_and_definition,
    get_value_at_path,
    set_value_at_path,
)
from sysreptor.utils.utils import is_unique, is_uuid

log = logging.getLogger(__name__)


class NotesConsumerBase(GenericCollabMixin, CollabConsumerBase):
    serializer_class = None
    initial_path = 'notes'

    def get_notes_queryset(self):
        raise NotImplementedError()

    def get_serializer(self, *args, **kwargs):
        return self.serializer_class(*args, **kwargs)

    @database_sync_to_async
    def get_initial_message(self):
        notes = list(self.get_notes_queryset()
            .order_by('created'))
        return {
            'type': CollabEventType.INIT,
            'client_id': self.client_id,
            'client_color': self.client_color,
            'version': max(n.updated.timestamp() for n in notes) if notes else 0,
            'data': {
                'notes': {n['id']: n for n in self.get_serializer(notes, many=True).data},
            },
            'clients': self.get_client_infos(),
            'permissions': {
                'read': True,
                'write': self.has_permission(action='write'),
            },
        }

    async def receive_json(self, content, **kwargs):
        event = None
        match content.get('type'):
            case CollabEventType.UPDATE_KEY:
                event = await self.collab_update_key(content)
            case CollabEventType.UPDATE_TEXT:
                event = await self.collab_update_text(content)
            case CollabEventType.AWARENESS:
                event = await self.collab_update_awareness(content)
            case _:
                raise ValueError(f'Invalid message type: {content.get("type")}')
        await self.send_colllab_event(event)

    def get_note_for_update(self, path, valid_paths=None):
        if not isinstance(path, str):
            raise ValidationError('Invalid path')
        path_parts = tuple(path.split('.'))
        if len(path_parts) < 3 or path_parts[0] != 'notes' or not is_uuid(path_parts[1]) or not (not valid_paths or '.'.join(path_parts[2:]) in valid_paths):
            raise ValidationError('Invalid path')
        note = self.get_notes_queryset() \
            .filter(note_id=path_parts[1]) \
            .select_for_update(of=['self'], no_key=True) \
            .first()
        if not note:
            raise ValidationError('Invalid path: ID not found')
        return note, path_parts[2:], None

    def get_object_for_update(self, content):
        match content.get('type'):
            case CollabEventType.UPDATE_KEY:
                valid_paths = {k for k, f in self.get_serializer().fields.items() if not f.read_only} - {'title', 'text'}
            case CollabEventType.UPDATE_TEXT:
                valid_paths=['title', 'text']
            case _:
                raise ValidationError('Invalid collab event type')
        return self.get_note_for_update(path=content.get('path'), valid_paths=valid_paths)

    def perform_update_key(self, obj, path, value, **kwargs):
        serializer = self.get_serializer(instance=obj, data={path[0]: value}, partial=True)
        serializer.is_valid(raise_exception=True)
        return serializer.save(), {}

    def perform_update_text(self, obj, path, changes, **kwargs):
        setattr(obj, path[0], changes.apply(getattr(obj, path[0]) or ''))
        obj.save()
        return obj, {}


class ProjectNotesConsumer(NotesConsumerBase):
    serializer_class = ProjectNotebookPageSerializer

    async def get_related_id(self):
        return self.scope['url_route']['kwargs']['project_pk']

    @property
    def group_name(self) -> str:
        return f'project_{self.related_id}'

    def get_project(self):
        return PentestProject.objects \
            .only_permitted(self.user) \
            .filter(id=self.related_id) \
            .first()

    def has_permission(self, action=None, **kwargs):
        project = self.get_project()
        if not self.user or not project:
            return False
        if action in ['connect', 'read']:
            return True
        elif action in ['write']:
            if project.readonly:
                return False
            elif self.user.is_guest and not configuration.GUEST_USERS_CAN_EDIT_PROJECTS and not self.user.is_admin and not self.user.is_project_admin:
                return False
            return True

    def get_notes_queryset(self):
        return ProjectNotebookPage.objects \
            .filter(project_id=self.related_id) \
            .annotate_is_shared() \
            .select_related('parent', 'assignee')


class SharedProjectNotesPublicConsumer(NotesConsumerBase):
    serializer_class = ProjectNotebookPageSerializer

    @database_sync_to_async
    def get_related_id(self):
        if share_info := self.get_share_info():
            return share_info.note.project_id
        return None

    @property
    def group_name(self) -> str:
        return f'project_{self.related_id}'

    def get_share_info(self):
        return ShareInfo.objects \
            .filter(id=self.scope['url_route']['kwargs']['shareinfo_pk']) \
            .only_active() \
            .select_related('note__project') \
            .first()

    def has_permission(self, action=None, **kwargs):
        obj = self.get_share_info()
        if not obj or not obj.is_active:
            return False
        if obj.password and str(obj.id) not in self.scope['session'].get('authorized_shareids', []):
            return False
        if action in ['connect', 'read']:
            return True
        elif action in ['write']:
            if not obj.permissions_write:
                return False
            elif obj.note.project.readonly:
                return False
            return True

    def get_notes_queryset(self):
        share_info = self.get_share_info()
        if not share_info:
            return ProjectNotebookPage.objects.none()
        return ProjectNotebookPage.objects \
            .child_notes_of(share_info.note) \
            .annotate(is_shared=models.Q(id=share_info.note.id))

    async def filter_events(self, events):
        @database_sync_to_async
        def get_allowed_notes():
            return list(self.get_notes_queryset())

        out = []
        for event in events:
            event_type = CollabEventType(event['type'])
            if event_type in [CollabEventType.INIT, CollabEventType.CONNECT, CollabEventType.DISCONNECT, CollabEventType.AWARENESS, CollabEventType.DELETE]:
                out.append(event)
            elif event_type in [CollabEventType.UPDATE_KEY, CollabEventType.UPDATE_TEXT, CollabEventType.CREATE]:
                path = event.get('path') or ''
                allowed_notes = await get_allowed_notes()
                if any(path.startswith(f'notes.{n.note_id}') for n in allowed_notes):
                    out.append(event)
            elif event_type in [CollabEventType.SORT]:
                allowed_note_ids = {str(n.note_id) for n in await get_allowed_notes()}
                out.append(event | {
                    'sort': [o for o in event.get('sort', []) if o['id'] in allowed_note_ids],
                })
            else:
                raise AttributeError(f'Unsupported event type: {event_type}')
        return out


class UserNotesConsumer(NotesConsumerBase):
    serializer_class = UserNotebookPageSerializer

    async def get_related_id(self):
        user_id = self.scope['url_route']['kwargs']['pentestuser_pk']
        if user_id == 'self':
            return getattr(self.user, 'id', None)
        else:
            return user_id

    @property
    def group_name(self) -> str:
        return f'user_{self.related_id}'

    def has_permission(self, **kwargs):
        if not self.user:
            return False
        if str(self.user.id) == str(self.related_id):
            return True
        return False

    def get_notes_queryset(self):
        return UserNotebookPage.objects \
            .filter(user_id=self.related_id) \
            .select_related('parent')


class NoteExcalidrawConsumerBase(CollabConsumerBase):
    initial_path = 'excalidraw'

    @property
    def group_name(self) -> str:
        return f'excalidraw_{self.related_id}'

    @database_sync_to_async
    def get_related_id(self):
        if note := self.get_note():
            return note.id
        return None

    def get_note(self):
        pass

    @database_sync_to_async
    def get_initial_message(self):
        note = self.get_note()
        elements = note.excalidraw_data.get('elements', [])
        return {
            'type': 'collab.init',
            'client_id': self.client_id,
            'elements': elements,
            'clients': self.get_client_infos(),
            'permissions': {
                'read': True,
                'write': self.has_permission(action='write'),
            },
        }

    async def receive_json(self, content, **kwargs):
        event = None
        match content.get('type'):
            case CollabEventType.UPDATE_EXCALIDRAW:
                event = await self.collab_update_excalidraw(content)
            case _:
                raise ValueError(f'Invalid message type: {content.get("type")}')
        await self.send_colllab_event(event)

    async def collab_update_excalidraw(self, content):
        elements = content.get('elements', [])
        sync_all = content.get('sync_all', False)

        timestamp = timezone.now()
        event = CollabEvent(
            related_id=self.related_id,
            path=self.initial_path,
            type=CollabEventType.UPDATE_EXCALIDRAW,
            created=timestamp,
            version=timestamp.timestamp(),
            client_id=self.client_id,
            data={
                'elements': elements,
                'sync_all': sync_all,
            },
        )

        max_message_size =  1024 * 1024 - 100  # 1MB without wrapper size
        exceeds_max_size = len(await self.encode_json(event.to_dict())) > max_message_size

        @database_sync_to_async
        def db_handler():
            if sync_all:
                # Update DB entries
                note = self.get_note()
                if note:
                    with collab_context(prevent_events=True):
                        note.update_excalidraw_data({'elements': elements})

            if exceeds_max_size:
                # Save to DB when the message is too large
                event.save()

        if sync_all or exceeds_max_size:
            await db_handler()
        return event if exceeds_max_size else event.to_dict()


class ProjectNoteExcalidrawConsumer(NoteExcalidrawConsumerBase):
    def get_note(self):
        return ProjectNotebookPage.objects \
            .filter(project_id=self.scope['url_route']['kwargs']['project_pk']) \
            .filter(note_id=self.scope['url_route']['kwargs']['note_id']) \
            .first()

    def get_project(self):
        return PentestProject.objects \
            .only_permitted(self.user) \
            .filter(id=self.scope['url_route']['kwargs']['project_pk']) \
            .first()

    def has_permission(self, action=None, **kwargs):
        project = self.get_project()
        if not self.user or not project:
            return False
        if action in ['connect', 'read']:
            return True
        elif action in ['write']:
            if project.readonly:
                return False
            elif self.user.is_guest and not configuration.GUEST_USERS_CAN_EDIT_PROJECTS and not self.user.is_admin and not self.user.is_project_admin:
                return False
            return True


class SharedProjectNoteExcalidrawConsumer(NoteExcalidrawConsumerBase):
    def get_share_info(self):
        return ShareInfo.objects \
            .filter(id=self.scope['url_route']['kwargs']['shareinfo_pk']) \
            .only_active() \
            .select_related('note__project') \
            .first()

    def get_notes_queryset(self):
        share_info = self.get_share_info()
        if not share_info:
            return ProjectNotebookPage.objects.none()
        return ProjectNotebookPage.objects \
            .child_notes_of(share_info.note) \
            .select_related('excalidraw_file')

    def get_note(self):
        return self.get_notes_queryset() \
            .filter(note_id=self.scope['url_route']['kwargs']['note_id']) \
            .first()

    def has_permission(self, action=None, **kwargs):
        obj = self.get_share_info()
        if not obj or not obj.is_active:
            return False
        if obj.password and str(obj.id) not in self.scope['session'].get('authorized_shareids', []):
            return False

        note = self.get_note()
        if not note or note.type != NoteType.EXCALIDRAW:
            return False

        if action in ['connect', 'read']:
            return True
        elif action in ['write']:
            if not obj.permissions_write:
                return False
            elif obj.note.project.readonly:
                return False
            return True


class UserNoteExcalidrawConsumer(NoteExcalidrawConsumerBase):
    def get_note(self):
        qs = UserNotebookPage.objects \
            .filter(note_id=self.scope['url_route']['kwargs']['note_id'])
        user_id = self.scope['url_route']['kwargs']['pentestuser_pk']
        if user_id == 'self':
            qs = qs.filter(user=self.user)
        else:
            qs = qs.filter(user_id=user_id)
        return qs.first()

    def has_permission(self, **kwargs):
        if not self.user:
            return False
        note = self.get_note()
        if not note or note.type != NoteType.EXCALIDRAW or note.user != self.user:
            return False
        return True


class ProjectReportingConsumer(GenericCollabMixin, CollabConsumerBase):
    async def get_related_id(self):
        return self.scope['url_route']['kwargs']['project_pk']

    @property
    def group_name(self) -> str:
        return f'project_{self.related_id}'

    def has_permission(self, action=None, **kwargs):
        project = self.get_project()
        if not self.user or not project:
            return False
        if action in ['connect', 'read']:
            return True
        elif action in ['write']:
            return ProjectSubresourcePermissions.has_write_permissions(project=project, user=self.user)

    def get_project(self, prefetch_related=False):
        qs = PentestProject.objects \
            .only_permitted(self.user) \
            .filter(id=self.related_id)
        if prefetch_related:
            comment_prefetch = Prefetch('comments', Comment.objects.select_related('user').prefetch_related(Prefetch('answers', CommentAnswer.objects.select_related('user'))))
            qs = qs \
                .select_related('project_type') \
                .prefetch_related(
                    Prefetch('sections', queryset=ReportSection.objects.select_related('assignee').prefetch_related(comment_prefetch)),
                    Prefetch('findings', queryset=PentestFinding.objects.select_related('assignee').prefetch_related(comment_prefetch)),
                )
        return qs.first()

    def filter_path(self, qs_or_obj):
        path_prefixes = ('findings', 'sections', 'comments', 'project')
        if isinstance(qs_or_obj, models.QuerySet):
            return qs_or_obj.filter(
                functools.reduce(operator.or_, [
                    models.Q(path=None),
                    models.Q(path=''),
                    *[models.Q(path__startswith=prefix) for prefix in path_prefixes],
                ]),
            )
        elif isinstance(qs_or_obj, dict):
            path = qs_or_obj.get('path') or ''
            if not path or path.startswith(path_prefixes):
                return qs_or_obj
        return None

    @database_sync_to_async
    def get_initial_message(self):
        project = self.get_project(prefetch_related=True)
        if not project:
            return None
        sections = list(project.sections.all())
        findings = list(project.findings.all())
        comments = list(itertools.chain(*(o.comments.all() for o in sections + findings)))
        return {
            'type': CollabEventType.INIT,
            'client_id': self.client_id,
            'client_color': self.client_color,
            'version': max([o.updated.timestamp() for o in sections + findings + [project]]),
            'data': {
                'project': {
                    'id': project.id,
                    'project_type': project.project_type.id,
                    'override_finding_order': project.override_finding_order,
                },
                'sections': {s['id']: s for s in ReportSectionSerializer(sections, many=True).data},
                'findings': {f['id']: f for f in PentestFindingSerializer(findings, many=True).data},
                'comments': {c['id']: c for c in CommentSerializer(comments, many=True).data} if license.is_professional() else {},
            },
            'clients': self.get_client_infos(),
            'permissions': {
                'read': True,
                'write': self.has_permission(action='write'),
            },
        }

    async def receive_json(self, content, **kwargs):
        event = None
        match content.get('type'):
            case CollabEventType.UPDATE_KEY:
                event = await self.collab_update_key(content)
            case CollabEventType.UPDATE_TEXT:
                event = await self.collab_update_text(content)
            case CollabEventType.AWARENESS:
                event = await self.collab_update_awareness(content)
            case CollabEventType.CREATE:
                event = await self.collab_create(content)
            case CollabEventType.DELETE:
                event = await self.collab_delete(content)
            case _:
                raise ValueError(f'Invalid message type: {content.get("type")}')
        await self.send_colllab_event(event)

    def _get_object_for_update(self, path):
        if not isinstance(path, str):
            raise ValidationError('Invalid path')
        path_parts = tuple(path.split('.'))
        if len(path_parts) < 3 or not (path_parts[0] == 'sections' or (path_parts[0] == 'findings' and is_uuid(path_parts[1]))):
            raise ValidationError('Invalid path')

        if path_parts[0] == 'sections':
            obj_qs = ReportSection.objects.filter(section_id=path_parts[1])
            serializer_class = ReportSectionSerializer
        elif path_parts[0] == 'findings' and is_uuid(path_parts[1]):
            obj_qs = PentestFinding.objects.filter(finding_id=path_parts[1])
            serializer_class = PentestFindingSerializer
        else:
            raise ValidationError('Invalid path')

        obj = obj_qs \
            .filter(project_id=self.related_id) \
            .select_related('assignee', 'project__project_type') \
            .prefetch_related('comments') \
            .select_for_update(of=['self'], no_key=True) \
            .first()
        if not obj:
            raise ValidationError('Invalid path: ID not found')

        # Validate path in top-level or in field definition
        if path_parts[2] == 'data':
            try:
                _, _, definition = get_field_value_and_definition(data=obj.data, definition=obj.field_definition, path=path_parts[3:])
                return obj, path_parts[2:], definition
            except KeyError as ex:
                raise ValidationError('Invalid path') from ex
        else:
            valid_paths = {k for k, f in serializer_class().fields.items() if not f.read_only}
            if len(path_parts) > 3 or path_parts[2] not in valid_paths:
                raise ValidationError('Invalid path')

            return obj, path_parts[2:], None

    def get_object_for_update(self, content):
        obj, path, definition = self._get_object_for_update(content.get('path'))
        match content.get('type'):
            case CollabEventType.UPDATE_TEXT:
                if not definition or definition.type not in [FieldDataType.MARKDOWN, FieldDataType.STRING]:
                    raise ValidationError('collab.update_text is not supported for non-text fields. Use collab.update_key instead.')
            case CollabEventType.UPDATE_KEY:
                # Allow for all field types
                pass
            case CollabEventType.CREATE:
                if not definition or definition.type != FieldDataType.LIST:
                    raise ValidationError('collab.create is only supported for list fields')
            case CollabEventType.DELETE:
                if not definition:
                    raise ValidationError('collab.delete is only supported for list fields')
            case _:
                raise ValidationError('Invalid collab event type')
        return obj, path, definition

    def perform_update_text(self, obj, path, changes, **kwargs):
        # Update data
        updated_data = obj.data
        set_value_at_path(updated_data, path[1:], changes.apply(get_value_at_path(updated_data, path[1:]) or ''))
        obj.update_data(updated_data)
        obj.save()

        # Update comment positions
        comments_to_update = []
        for c in obj.comments.all():
            if c.path == '.'.join(path) and c.text_range:
                try:
                    c.text_range = c.text_range.map(changes)
                except ValueError:
                    c.text_range = None
                comments_to_update.append(c)
        Comment.objects.bulk_update(comments_to_update, ['text_range_from', 'text_range_to'])

        return obj, {
            'comments': [c.to_dict_short() for c in comments_to_update] if license.is_professional() else [],
        }

    def perform_update_key(self, obj, path, value, definition, content, **kwargs):
        # Update data in DB
        comments_to_update = []

        if definition:
            updated_data = obj.data

            # Handle comments
            comments = list(obj.comments.all())
            if (sort_data := content.get('sort')) and definition.type == FieldDataType.LIST:
                value_old = get_value_at_path(updated_data, path[1:])

                # Validate sort data
                if not (
                    isinstance(sort_data, list)
                    and all(isinstance(d, dict) and isinstance(d.get('id'), int) and isinstance(d.get('order'), int) and 0 <= d['id'] < len(value_old) and 0 <= d['order'] < len(value) for d in sort_data)
                    and is_unique(map(lambda d: d['id'], sort_data)) and is_unique(map(lambda d: d['order'], sort_data))
                ):
                    raise ValidationError('Invalid sort infos')

                # Move comments to index spcified in update event
                for d in sort_data:
                    for c in comments:
                        path_old = '.'.join(path + (f'[{d["id"]}]',))
                        path_new = '.'.join(path + (f'[{d["order"]}]',))
                        if c.path == path_old or c.path.startswith(f'{path_old}.'):
                            c.path = path_new + c.path[len(path_old):]
                            c.text_range = None
                            comments_to_update.append(c)
                            comments.remove(c)
            else:
                # Move comment up to the greatest static path part without list indices in them
                for c in comments:
                    c_path = tuple(c.path.split('.'))
                    if c_path[:len(path)] == path:
                        static_path = c_path[:len(path)] + tuple(itertools.takewhile(lambda p: not (p.startswith('[') and p.endswith(']')), c_path[len(path):]))
                        c.path = '.'.join(static_path)
                        c.text_range = None
                        comments_to_update.append(c)
                        comments.remove(c)

            # Update data
            set_value_at_path(updated_data, path[1:], value)
            serializer_data = {'data': updated_data}
        else:
            # Top-level fields, not in custom data
            serializer_data = {path[0]: value}

        # Update in DB
        serializer_class = ReportSectionSerializer if isinstance(obj, ReportSection) else PentestFindingSerializer
        serializer = serializer_class(instance=obj, data=serializer_data, partial=True, context={'project': obj.project, 'user': self.user})
        serializer.is_valid(raise_exception=True)
        res = serializer.save()

        # Update comment positions
        Comment.objects.bulk_update(comments_to_update, fields=['path', 'text_range_from', 'text_range_to'])

        return res, {
            'comments': [c.to_dict_short() for c in comments_to_update] if license.is_professional() else [],
        }

    @database_sync_to_async
    @transaction.atomic()
    def collab_create(self, content):
        if content.get('path') == 'comments':
            if not license.is_professional():
                raise ValidationError('Professional license required')

            # Create comment
            serializer = CommentCreateSerializer(data=content.get('value'), context={
                'project': self.get_project(prefetch_related=True),
                'version': content.get('version'),
                'user': self.user,
            })
            serializer.is_valid(raise_exception=True)
            serializer.save()

            # Event is emitted by signal handler
            return None
        else:
            # Create list item
            obj, path, _ = self.get_object_for_update(content)

            # Update DB
            updated_data = obj.data
            lst = get_value_at_path(updated_data, path[1:])
            lst.append(content.get('value'))
            index = len(lst) - 1
            serializer = (ReportSectionSerializer if isinstance(obj, ReportSection) else PentestFindingSerializer)(instance=obj, data={'data': updated_data}, partial=True)
            serializer.is_valid(raise_exception=True)
            with collab_context(prevent_events=True):
                obj = serializer.save()

            return CollabEvent.objects.create(
                related_id=self.related_id,
                path=f"{content['path']}.[{index}]",
                type=CollabEventType.CREATE,
                created=obj.updated,
                version=obj.updated.timestamp(),
                client_id=self.client_id,
                data={
                    'value': content['value'],
                },
            )

    @database_sync_to_async
    @transaction.atomic()
    def collab_delete(self, content):
        obj, path, _ = self.get_object_for_update(content)

        # Validate list index and delete list item
        updated_data = obj.data
        lst = get_value_at_path(updated_data, path[1:-1])
        if not isinstance(lst, list):
            raise ValidationError('collab.delete is only supported for list fields')
        index = int(path[-1][1:-1] if path[-1].startswith('[') and path[-1].endswith(']') else path[-1])
        if not (0 <= index < len(lst)):
            raise ValidationError('Invalid list index')
        lst.pop(index)
        serializer = (ReportSectionSerializer if isinstance(obj, ReportSection) else PentestFindingSerializer)(instance=obj, data={'data': updated_data}, partial=True)
        serializer.is_valid(raise_exception=True)
        with collab_context(prevent_events=True):
            obj = serializer.save()

            # Delete comments
            comments_to_delete = []
            comments_to_update = []
            for c in obj.comments.all():
                c_path = tuple(c.path.split('.'))
                if c_path[:len(path)] == path:
                    comments_to_delete.append(c)
                elif c_path[:len(path) - 1] == path[:-1] and len(c_path) >= len(path) and (c_idx := int(c_path[len(path) - 1][1:-1])) > index:
                    c_path[len(path) - 1] = f'[{c_idx}]'
                    c.path = '.'.join(c_path)
                    comments_to_update.append(c)

            Comment.objects.filter(pk__in=map(lambda c: c.id, comments_to_delete)).delete()
            Comment.objects.bulk_update(comments_to_update, fields=['path'])

        return CollabEvent.objects.create(
            related_id=self.related_id,
            path=content['path'],
            type=CollabEventType.DELETE,
            created=obj.updated,
            version=obj.updated.timestamp(),
            client_id=self.client_id,
            data={
                'comments': [{'id': c.id, 'path': None} for c in comments_to_delete] + [{'id': c.id, 'path': c.path_absolute} for c in comments_to_update],
            },
        )


def send_collab_event_project(event: CollabEvent):
    group_name = f'project_{event.related_id}'
    layer = get_channel_layer()
    if layer:
        async_to_sync(layer.group_send)(group_name, {
            'type': 'collab_event',
            'id': str(event.id),
            'path': event.path,
        })


def send_collab_event_user(event: CollabEvent):
    group_name = f'user_{event.related_id}'
    layer = get_channel_layer()
    if layer:
        async_to_sync(layer.group_send)(group_name, {
            'type': 'collab_event',
            'id': str(event.id),
            'path': event.path,
        })


def send_collab_event_excalidraw(event: CollabEvent):
    group_name = f'excalidraw_{event.related_id}'
    layer = get_channel_layer()
    if layer:
        async_to_sync(layer.group_send)(group_name, {
            'type': 'collab_event',
            'id': str(event.id),
            'path': event.path,
        })
